import { Connection, PublicKey, SystemProgram, TransactionInstruction } from '@solana/web3.js';

import {
    createInstruction,
    deleteInstruction,
    reallocInstruction,
    transferInstruction,
    updateInstruction,
} from './instructions';
import { NameRegistryState } from './state';
import { Numberu64 } from './utils';
import { getHashedName, getNameAccountKey, getNameOwner, Numberu32 } from './utils';

////////////////////////////////////////////////////////////

export const NAME_PROGRAM_ID = new PublicKey('namesLPneVptA9Z5rqUDD9tMTWEJwofgaYwp8cawRkX');
export const HASH_PREFIX = 'SPL Name Service';

////////////////////////////////////////////////////////////
/**
 * Creates a name account with the given rent budget, allocated space, owner and class.
 *
 * @param connection The solana connection object to the RPC node
 * @param name The name of the new account
 * @param space The space in bytes allocated to the account
 * @param payerKey The allocation cost payer
 * @param nameOwner The pubkey to be set as owner of the new name account
 * @param lamports The budget to be set for the name account. If not specified, it'll be the minimum for rent exemption
 * @param nameClass The class of this new name
 * @param parentName The parent name of the new name. If specified its owner needs to sign
 * @returns
 */
export async function createNameRegistry(
    connection: Connection,
    name: string,
    space: number,
    payerKey: PublicKey,
    nameOwner: PublicKey,
    lamports?: number,
    nameClass?: PublicKey,
    parentName?: PublicKey,
): Promise<TransactionInstruction> {
    const hashed_name = await getHashedName(name);
    const nameAccountKey = await getNameAccountKey(hashed_name, nameClass, parentName);

    const balance = lamports ? lamports : await connection.getMinimumBalanceForRentExemption(space);

    let nameParentOwner: PublicKey | undefined;
    if (parentName) {
        const parentAccount = await getNameOwner(connection, parentName);
        nameParentOwner = parentAccount.owner;
    }

    const createNameInstr = createInstruction(
        NAME_PROGRAM_ID,
        SystemProgram.programId,
        nameAccountKey,
        nameOwner,
        payerKey,
        hashed_name,
        new Numberu64(balance),
        new Numberu32(space),
        nameClass,
        parentName,
        nameParentOwner,
    );

    return createNameInstr;
}

/**
 * Overwrite the data of the given name registry.
 *
 * @param connection The solana connection object to the RPC node
 * @param name The name of the name registry to update
 * @param offset The offset to which the data should be written into the registry
 * @param input_data The data to be written
 * @param nameClass The class of this name, if it exsists
 * @param nameParent The parent name of this name, if it exists
 */
export async function updateNameRegistryData(
    connection: Connection,
    name: string,
    offset: number,
    input_data: Buffer,
    nameClass?: PublicKey,
    nameParent?: PublicKey,
): Promise<TransactionInstruction> {
    const hashed_name = await getHashedName(name);
    const nameAccountKey = await getNameAccountKey(hashed_name, nameClass, nameParent);

    let signer: PublicKey;
    if (nameClass) {
        signer = nameClass;
    } else {
        signer = (await NameRegistryState.retrieve(connection, nameAccountKey)).owner;
    }

    const updateInstr = updateInstruction(
        NAME_PROGRAM_ID,
        nameAccountKey,
        new Numberu32(offset),
        input_data,
        signer,
        nameParent,
    );

    return updateInstr;
}

/**
 * Change the owner of a given name account.
 *
 * @param connection The solana connection object to the RPC node
 * @param name The name of the name account
 * @param newOwner The new owner to be set
 * @param curentNameOwner the current name Owner
 * @param nameClass The class of this name, if it exsists
 * @param nameParent The parent name of this name, if it exists
 * @returns
 */
export async function transferNameOwnership(
    connection: Connection,
    name: string,
    newOwner: PublicKey,
    nameClass?: PublicKey,
    nameParent?: PublicKey,
): Promise<TransactionInstruction> {
    const hashed_name = await getHashedName(name);
    const nameAccountKey = await getNameAccountKey(hashed_name, nameClass, nameParent);

    let curentNameOwner: PublicKey;
    if (nameClass) {
        curentNameOwner = nameClass;
    } else {
        curentNameOwner = (await NameRegistryState.retrieve(connection, nameAccountKey)).owner;
    }

    const transferInstr = transferInstruction(
        NAME_PROGRAM_ID,
        nameAccountKey,
        newOwner,
        curentNameOwner,
        nameClass,
        nameParent,
    );

    return transferInstr;
}

/**
 * Delete the name account and transfer the rent to the target.
 *
 * @param connection The solana connection object to the RPC node
 * @param name The name of the name account
 * @param refundTargetKey The refund destination address
 * @param nameClass The class of this name, if it exsists
 * @param nameParent The parent name of this name, if it exists
 * @returns
 */
export async function deleteNameRegistry(
    connection: Connection,
    name: string,
    refundTargetKey: PublicKey,
    nameClass?: PublicKey,
    nameParent?: PublicKey,
): Promise<TransactionInstruction> {
    const hashed_name = await getHashedName(name);
    const nameAccountKey = await getNameAccountKey(hashed_name, nameClass, nameParent);

    let nameOwner: PublicKey;
    if (nameClass) {
        nameOwner = nameClass;
    } else {
        nameOwner = (await NameRegistryState.retrieve(connection, nameAccountKey)).owner;
    }

    const changeAuthoritiesInstr = deleteInstruction(NAME_PROGRAM_ID, nameAccountKey, refundTargetKey, nameOwner);

    return changeAuthoritiesInstr;
}

/**
 * Realloc the name account space.
 *
 * @param connection The solana connection object to the RPC node
 * @param name The name of the name account
 * @param space The new space to be allocated
 * @param payerKey The allocation cost payer if new space is larger than current or the refund destination if smaller
 * @param nameClass The class of this name, if it exsists
 * @param nameParent The parent name of this name, if it exists
 * @returns
 */
export async function reallocNameAccount(
    connection: Connection,
    name: string,
    space: number,
    payerKey: PublicKey,
    nameClass?: PublicKey,
    nameParent?: PublicKey,
): Promise<TransactionInstruction> {
    const hashedName = await getHashedName(name);
    const nameAccountKey = await getNameAccountKey(hashedName, nameClass, nameParent);

    let nameOwner: PublicKey;
    if (nameClass) {
        nameOwner = nameClass;
    } else {
        nameOwner = (await NameRegistryState.retrieve(connection, nameAccountKey)).owner;
    }

    const reallocInstr = reallocInstruction(
        NAME_PROGRAM_ID,
        SystemProgram.programId,
        payerKey,
        nameAccountKey,
        nameOwner,
        new Numberu32(space),
    );

    return reallocInstr;
}
